package com.medistat.dialog.services;

import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.stereotype.Service;

import com.medistat.dialog.Schedule;
import com.medistat.dialog.ScheduleCell;
import com.medistat.dialog.ScheduleSelection;
import com.medistat.model.Doctor;
import com.medistat.model.FreeDay;
import com.medistat.model.Holiday;
import com.medistat.model.Person;
import com.medistat.model.WeekendDutiesInfo;
import com.medistat.model.Work;
import com.medistat.model.enums.Days;
import com.medistat.model.enums.TypeOfWork;
import com.medistat.model.enums.WeekendDutiesInfoCreationPurpose;
import com.medistat.util.DateUtils;
import com.medistat.util.DaysUtils;
import com.medistat.util.TypeOfWorkUtils;

@Service("createScheduleStrategy")
public class CreateScheduleStrategy extends ScheduleCreator {
	
	protected Map<Long, Integer> dutiesCache;
	protected Map<Calendar, Holiday> holidaysCache;
	protected List<Doctor> doctorsCache;
	protected int indexForRandomSearchingDoctor = 0;
	
	// second pass is needed that once we have all regular work we can assign all duties
	@Override
	protected void secondPass(List<com.medistat.dialog.Schedule> schedules) throws Exception {
		for (com.medistat.dialog.Schedule schedule : schedules) {
			iterateThroughScheduleSelection(schedule);
		}
		//duty doctor assignment for last day in month should be done regarding with first day in next month
		Schedule lastScheduleInMonth = schedules.get(schedules.size()-1);
		lastDayInMonthDutyAssignement(lastScheduleInMonth);
		
		//first day can be also extra handled if already exists
		firstDayInMonthDutyAssignment(schedules);
	}
	
	//extra handling first day if is neccessery.
	private void firstDayInMonthDutyAssignment(List<Schedule> schedules) {
		//schedule with 0 position can be from previous month - so I took position 1
		Calendar firstDateInMonth = DateUtils.getMinDate(schedules.get(1).getScheduleSelections().get(0).getScheduleCells().get(0).getDate());
		Days firstDayInMonth = DateUtils.getDayFrom(firstDateInMonth);
		Calendar oneDayBefore = DateUtils.subtractDate(firstDateInMonth, 1);
		
		Work work = persistanceService.findWork(oneDayBefore, TypeOfWork.DUTY);
		//relevant just if work exists and is sunday - then doctor is the same as saturday.
		if(work!=null && DateUtils.isSunday(firstDayInMonth)) {
			List<ScheduleCell> yesterdayScheduleCells = schedules.get(0).getCellsForDay(DateUtils.getDayFrom(oneDayBefore));
			List<ScheduleCell> todayScheduleCells = schedules.get(0).getCellsForDay(DateUtils.getDayFrom(firstDateInMonth));
			
			ScheduleCell yesterdayDuty = yesterdayScheduleCells.get(yesterdayScheduleCells.size() - 1); // duty assume that is last one - potentional problems
			ScheduleCell todayDuty = todayScheduleCells.get(todayScheduleCells.size() - 1); // duty assume that is last one - potentional problems
			todayDuty.setDoctor(yesterdayDuty.getDoctor());
			todayDuty.setDoctorId(yesterdayDuty.getDoctor() == null ? null : yesterdayDuty.getDoctor().getId());
			lockScheduleCells(todayScheduleCells);
			
		}
	}
	
	private void lockScheduleCells(List<ScheduleCell> scheduleCells) {
		for(ScheduleCell scheduleCell : scheduleCells) {
			scheduleCell.setActive(false);
		}
	}
	
	private void lastDayInMonthDutyAssignement(Schedule schedule) {
		
		Calendar firstDateInLastScheduleForMonth = schedule.getScheduleSelections().get(0).getScheduleCells().get(0).getDate();
		Calendar lastDate = DateUtils.getMaxDate(firstDateInLastScheduleForMonth);
		Days lastDay = DateUtils.getDayFrom(lastDate);
		
		//friday, sunday - last day should not be extra handled
		if(DateUtils.isFriday(lastDay) || DateUtils.isSunday(lastDay)) {
			return;
		}

		Calendar today = (Calendar) lastDate.clone();
		List<ScheduleCell> todayScheduleCells = schedule.getCellsForDay(DateUtils.getDayFrom(today));
		
		Calendar tommorow = DateUtils.addDate(lastDate, 1);
		List<ScheduleCell> tommorowScheduleCells = schedule.getCellsForDay(DateUtils.getDayFrom(tommorow));
		
		Doctor doctor = null;
		
		//saturday  - copy doctor from saturday to sunday (first date in next month)
		if(DateUtils.isSaturday(lastDay)) {
			ScheduleCell dutyScheduleCellSaturday = todayScheduleCells.get(todayScheduleCells.size() - 1); // duty assume that is last one - potentional problems
			doctor = dutyScheduleCellSaturday.getDoctor();
			
			ScheduleCell dutyScheduleCellSunday = tommorowScheduleCells.get(tommorowScheduleCells.size() - 1); // duty assume that is last one - potentional problems
			dutyScheduleCellSunday.setDoctor(doctor);
			dutyScheduleCellSunday.setDoctorId(doctor==null ? null : doctor.getId());
			dutyScheduleCellSunday.setActive(true);
		} else {
			//all other days
			doctor = findDoctorAppropriateDoctor(tommorowScheduleCells, todayScheduleCells);
			ScheduleCell dutyScheduleCell = todayScheduleCells.get(todayScheduleCells.size() - 1); // duty assume that is last one - potentional problems
			dutyScheduleCell.setDoctor(doctor);
			dutyScheduleCell.setDoctorId(doctor==null ? null : doctor.getId());
		}
		
	}

	private void iterateThroughScheduleSelection(com.medistat.dialog.Schedule schedule) throws Exception {
		for (ScheduleSelection scheduleSelection : schedule.getScheduleSelections()) {
			iterateThroughScheduleCells(schedule, scheduleSelection);
		}
	}

	private void iterateThroughScheduleCells(com.medistat.dialog.Schedule schedule, ScheduleSelection scheduleSelection) throws Exception {
		//handle just DUTY type of work 
		if (scheduleSelection.getTypeOfWork()!=null && scheduleSelection.getTypeOfWork().equals(TypeOfWork.DUTY)) {
			for (ScheduleCell scheduleCell : scheduleSelection.getScheduleCells()) {
				if (!scheduleCell.isActive()) {
					continue;
				}
				Doctor doctor = null;
				Days day = scheduleCell.getDay();
				Calendar date = scheduleCell.getDate();
				FreeDay vacation = null;
				if (DaysUtils.isSaturday(day) || isHoliday(date)) {
					// schedule doctors with less duties for saturday or holiday
					doctor = findDoctorWithLessDuitiesInThisYear();
					//check for vacations
					vacation = findFreeDayForDoctorOn(date, doctor);
					while(vacation!=null) {	
						doctor = randomFindDoctor();	//should random search if doctor with less duties in this year has free day.
						vacation = findFreeDayForDoctorOn(date, doctor);
					}
					if(doctor!=null) {
						incrementDutiesForDoctor(doctor.getId());
					}
				} else if (DaysUtils.isSunday(day)) {
					//copy settings from saturday
					Calendar yesterday = DateUtils.subtractDate(date, 1);
					List<ScheduleCell> yesterdayScheduleCells = schedule.getCellsForDay(DateUtils.getDayFrom(yesterday));
					ScheduleCell dutyScheduleCell = yesterdayScheduleCells.get(yesterdayScheduleCells.size() - 1); // duty assume that is last one - potentional problems
					doctor = dutyScheduleCell.getDoctor();
					//case when saturday duty doesn't have doctor assigned is when sunday is on first date in month and schedule for previous month was not generated.
					if(doctor==null) {
						doctor = randomFindDoctor();
					}
					if(doctor!=null) {
						incrementDutiesForDoctor(doctor.getId());
					}
				} else if (DateUtils.isWorkingWeek(day)) {
					// check work for tomorrow, today and assign doctor by some apple pie order.
					Calendar tomorrow = DateUtils.addDate(date, 1);
					List<ScheduleCell> tomorrowScheduleCells = schedule.getCellsForDay(DateUtils.getDayFrom(tomorrow));
					List<ScheduleCell> todayScheduleCells = schedule.getCellsForDay(DateUtils.getDayFrom(date));
					do {
						doctor = findDoctorAppropriateDoctor(tomorrowScheduleCells, todayScheduleCells);
						//check for vacations
						vacation = findFreeDayForDoctorOn(date, doctor);
					}
					while(vacation!=null);
				}
				scheduleCell.setDoctor(doctor);
				scheduleCell.setDoctorId(doctor == null ? null : doctor.getId());
			}
		}
	}
	
	private Doctor findDoctorWithLessDuitiesInThisYear() throws Exception {
		Integer lowerValue = null;
		Long doctorWithLessDuities = null;
		for (Map.Entry<Long, Integer> entry : dutiesCache.entrySet()) {
			if (doctorWithLessDuities == null || entry.getValue() < lowerValue) {
				doctorWithLessDuities = entry.getKey();
				lowerValue = entry.getValue();
			}
		}
		return persistanceService.getDoctor(doctorWithLessDuities);
	}
	
	private void incrementDutiesForDoctor(Long doctorWithLessDuities) {
		if (dutiesCache == null && doctorWithLessDuities == null) {
			return;
		}
		Integer lowerValue = dutiesCache.get(doctorWithLessDuities);
		dutiesCache.put(doctorWithLessDuities, lowerValue + 1);
	}
	
	private Doctor findDoctorAppropriateDoctor(List<ScheduleCell> tomorrowScheduleCells, List<ScheduleCell> todayScheduleCells) {
		Doctor doctor = null;
		boolean canBeScheduled = true;
		do {
			doctor = randomFindDoctor();			
			canBeScheduled = canDoctorBeScheduledForTomorrow(tomorrowScheduleCells, doctor);
			if(canBeScheduled) {
				canBeScheduled = canDoctorBeScheduledForToday(todayScheduleCells, doctor);
			}
		} while (!canBeScheduled);
		return doctor;
	}
	
	private boolean canDoctorBeScheduledForTomorrow(List<ScheduleCell> tomorrowScheduleCells, Doctor doctor) {
		boolean canBeScheduled = true;
		for (ScheduleCell scheduleCell : tomorrowScheduleCells) {
			Doctor doctorAssignedForTomorrow = scheduleCell.getDoctor();
			if (doctorAssignedForTomorrow == null) {
				continue;
			}
			if (doctor.getId().equals(doctorAssignedForTomorrow.getId())) {
				//duty can be assigned to doctor which is tomorrow on amb ptuj or erektilna disfunkcija 
				if(TypeOfWorkUtils.isAmbulantaPtuj(scheduleCell.getParent().getTypeOfWork()) || TypeOfWorkUtils.isErektilnaDisfunkcija(scheduleCell.getParent().getTypeOfWork())) {
					continue;
				} else {
					canBeScheduled = false;
					break;
				}
			}
		}
		return canBeScheduled;
	}
	
	private boolean canDoctorBeScheduledForToday(List<ScheduleCell> todayScheduleCells, Doctor doctor) {
		boolean canBeScheduled = true;
		for (ScheduleCell scheduleCell : todayScheduleCells) {
			Doctor doctorAssignedForTomorrow = scheduleCell.getDoctor();
			if (doctorAssignedForTomorrow == null) {
				continue;
			}
			if (doctor.getId().equals(doctorAssignedForTomorrow.getId())) {
				//duty can not be assigned to doctor which is today on amb ptuj or erektilna disfunkcija 
				if(TypeOfWorkUtils.isAmbulantaPtuj(scheduleCell.getParent().getTypeOfWork()) || TypeOfWorkUtils.isErektilnaDisfunkcija(scheduleCell.getParent().getTypeOfWork())) {
					canBeScheduled = false;
					break;
				}
			}
		}
		return canBeScheduled;
	}
	
	private Doctor randomFindDoctor() {
		if (this.indexForRandomSearchingDoctor == 0) {
			this.indexForRandomSearchingDoctor = this.doctorsCache.size() - 1;
		}
		return this.doctorsCache.get(indexForRandomSearchingDoctor--);
	}

	@Override
	protected Doctor findAvailableDoctor(Calendar date, TypeOfWork typeOfWork, ScheduleCell scheduleCell) throws Exception {
		Days day = DateUtils.getDayFrom(date);
		Integer weekInMonth = DateUtils.getWeekInMonthFrom(date);
		Doctor doctor = null;
		if (TypeOfWorkUtils.isDuty(typeOfWork) || DateUtils.isWeekend(day) || isHoliday(date) || restrictionForErektilnaExceeded(date, weekInMonth, typeOfWork) 
				|| restrictionForCistoskopijaExceeded(weekInMonth, typeOfWork)) {
			// because of first pass - duties are extra handled in second pass
			doctor = null;
		} else {
			return findAvailableDoctorFor(typeOfWork, weekInMonth, day, date);
			
		}
		return doctor;
	}
	
	private Doctor findAvailableDoctorFor(TypeOfWork typeOfWork, Integer weekInMonth, Days day, Calendar date) {
		Doctor doctor = null;
		if(TypeOfWorkUtils.isAmbulantaPtuj(typeOfWork) && weekInMonth > 3) {
			weekInMonth = weekInMonth % 3;
		} else if (weekInMonth > 4) {
			//settings like AMB_1, AMB_2 etc are repeatable
			weekInMonth = weekInMonth % 4;
		} else if (TypeOfWorkUtils.isErektilnaDisfunkcija(typeOfWork)) {
			weekInMonth = 1;
		}
		doctor = persistanceService.findDoctorFromSettings(day, weekInMonth, typeOfWork);
		
		if (doctor != null) {
			//check if doctor is not active anyomore
			if(!doctor.getActive()) {
				doctor=null;
			} else {
				//check if doctor has vacations
				FreeDay vacation = findFreeDayForDoctorOn(date, doctor);
				if (vacation != null) {
					doctor = findSubstituteForDoctor();
				}
			}
		}
		
		return doctor;
	}
	
	private Map<Long, Integer> initializeDutiesForDoctor() {
		Map<Long, Integer> map = new HashMap<Long, Integer>();
		List<Doctor> doctors = persistanceService.getActiveDoctors();
		this.doctorsCache = doctors;
		for (Doctor doctor : doctors) {
			map.put(doctor.getId(), Integer.valueOf(0));
		}
		return map;
	}
	
	private Map<Calendar, Holiday> getAllHolidaysForMonth(Calendar firstDate) {
		List<Holiday> holidays = persistanceService.getAllHolidaysForMonth(firstDate);
		Map<Calendar, Holiday> map = new HashMap<Calendar, Holiday>();
		if (holidays == null) {
			return map;
		}
		for (Holiday holiday : holidays) {
			map.put(holiday.getDate(), holiday);
		}
		return map;
	}

	private Map<Long, Integer> getAllDutiesForAllDoctorsInYear(Integer year) {
		Map<Long, Integer> map = initializeDutiesForDoctor();
		List<WeekendDutiesInfo> weekendDutiesInfos = persistanceService.getWeekendDutiesInfosWithMaxCreationDateForDoctors(doctorsCache);
		for(WeekendDutiesInfo weekendDutiesInfo : weekendDutiesInfos) {
			Person doctor = weekendDutiesInfo.getPerson();
			Integer numberOfDuties = weekendDutiesInfo.getNumberOfDuties();
			map.put(doctor.getId(), numberOfDuties + 1);
		}
		return map;
	}
	
	protected boolean isHoliday(Calendar date) {
		if (holidaysCache.containsKey(date)) {
			return true;
		}
		return false;
	}
	
	@Override
	protected void beforeCreation(Integer year, Calendar date) {
		this.dutiesCache = getAllDutiesForAllDoctorsInYear(year);
		this.holidaysCache = getAllHolidaysForMonth(date);
		
		//if weekend duties for this year not exists yet we should add new weekend duties with initial values.
		WeekendDutiesInfo weekendDutiesInfo = persistanceService.getWeekendDutiesInfo(year, WeekendDutiesInfoCreationPurpose.NEW_YEAR);
		if(weekendDutiesInfo==null) {
			try {
				persistanceService.addWeekendDutiesInfo(WeekendDutiesInfoCreationPurpose.NEW_YEAR, WeekendDutiesInfoCreationPurpose.NEW_YEAR.toString(), Calendar.getInstance());
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
	}

	@Override
	protected void afterCreation() {
		this.dutiesCache = null;
		this.holidaysCache = null;
		this.doctorsCache = null;
		this.indexForRandomSearchingDoctor = 0;
	}

	private FreeDay findFreeDayForDoctorOn(Calendar date, Doctor doctor) {
		return persistanceService.findFreeDayForDoctorOn(date, doctor);
	}

	// TODO: implement according to customer wish.
	private Doctor findSubstituteForDoctor() {
		return null;
	}

	private boolean restrictionForErektilnaExceeded(Calendar date, Integer weekInMonth, TypeOfWork typeOfWork) {
		return TypeOfWorkUtils.isErektilnaDisfunkcija(typeOfWork) && !DateUtils.isFirstThursdayInMonth(date);
		//return weekInMonth > 1 && TypeOfWorkUtils.isErektilnaDisfunkcija(typeOfWork);
	}
	
	private boolean restrictionForCistoskopijaExceeded(Integer weekInMonth, TypeOfWork typeOfWork) {
		return weekInMonth > 4 && TypeOfWorkUtils.isCistoskopija(typeOfWork);
	}
}
